/*
 * Copyright (c) 2017-present, CV4J Contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.cv4j.core.binary;

import com.cv4j.core.datamodel.ByteProcessor;
import com.cv4j.core.datamodel.Line;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 脚腕linesp
 *
 * @author Administrator
 * @date 2021/07/27
 */
public class HoughLinesP {
    /**
     * coslut
     */
    private double[] coslut;
    /**
     * sinlut
     */
    private double[] sinlut;
    /**
     * acc的大小
     */
    private int accSize;
    /**
     * 宽度
     */
    private int width;
    /**
     * 高度
     */
    private int height;
    /**
     * acc阈值
     */
    private int accThreshold;

    /**
     * 脚腕linesp
     */
    public HoughLinesP() {
        setupCosLUT();
        setupSinLUT();
    }


    /**
     * 过程
     * 1. 初始化霍夫变换空间
     * 2. 将图像的2D空间转换到霍夫空间,每个像素坐标都要转换到霍夫极坐标的对应强度值
     * 3. 找出霍夫极坐标空间的最大强度值
     * 4. 根据最大强度值归一化,范围为0 ~ 255
     * 5. 根据输入前accSize值,画出前accSize个信号最强的直线
     *
     * @param binary binary
     * @param accSize accSize
     * @param minGap minGap
     * @param minAcc minAcc
     * @param lines lines
     */
    public void process(ByteProcessor binary, int accSize, int minGap, int minAcc, List<Line> lines) {
        this.width = binary.getWidth();
        this.height = binary.getHeight();
        this.accSize = accSize; // 前K=accSize个累积值
        this.accThreshold = minAcc; // 最小累积值
        int rmax = (int) Math.sqrt(width * width + height * height);
        int[] acc = new int[rmax * 180]; // 0 ~ 180角度范围
        int r1;
        byte[] input = binary.getGray();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                if ((input[y * width + x] & 0xff) == 255) {
                    for (int theta = 0; theta < 180; theta++) {
                        r1 = (int) (x * coslut[theta] + y * sinlut[theta]); // 计算出极坐标
                        if ((r1 > 0) && (r1 <= rmax)) {
                            acc[r1 * 180 + theta] = acc[r1 * 180 + theta] + 1; // 在斜率范围内的点，极坐标相同
                        }
                    }
                }
            }
        }

        // 寻找最大值
        int max = 0;
        for (r1 = 0; r1 < rmax; r1++) {
            for (int theta = 0; theta < 180; theta++) {
                if (acc[r1 * 180 + theta] > max) {
                    // swap the max value
                    max = acc[r1 * 180 + theta];
                }
            }
        }

        // normalization all the values,
        int value;
        for (r1 = 0; r1 < rmax; r1++) {
            for (int theta = 0; theta < 180; theta++) {
                value = (int) (((double) acc[r1 * 180 + theta] / (double) max) * 255.0);
                acc[r1 * 180 + theta] = 0xff000000 | (value << 16 | value << 8 | value);
            }
        }
        findMaxima(acc, lines); // 发现前N个信号最强的点，转换为平面坐标，得到直线
    }

    /**
     * 找到极大值
     *
     * @param acc acc
     * @param lines 行
     */
    private void findMaxima(int[] acc, List<Line> lines) { // 初始化
        int rmax = (int) Math.sqrt(width * width + height * height);
        int[] results = new int[accSize * 3];
        for (int r = 0; r < rmax; r++) { // 开始寻找前N个最强信号点，记录极坐标坐标位置
            for (int theta = 0; theta < 180; theta++) {
                int value = (acc[r * 180 + theta] & 0xff);
                if (value > results[(accSize - 1) * 3]) { // add to bottom of array
                    results[(accSize - 1) * 3] = value;
                    results[(accSize - 1) * 3 + 1] = r;
                    results[(accSize - 1) * 3 + 2] = theta;
                    int i1 = (accSize - 2) * 3; // shift up until its in right place
                    while ((i1 >= 0) && (results[i1 + 3] > results[i1])) {
                        for (int j = 0; j < 3; j++) {
                            int temp = results[i1 + j];
                            results[i1 + j] = results[i1 + 3 + j];
                            results[i1 + 3 + j] = temp;
                        }
                        i1 = i1 - 3;
                        if (i1 < 0) {
                            break;
                        }
                    }
                }
            }
        }
        List<Line> tempLines = new ArrayList<>(); // 绘制像素坐标
        for (int i = accSize - 1; i >= 0; i--) {
            Line line = drawPolarLine(results[i * 3], results[i * 3 + 1], results[i * 3 + 2]);
            tempLines.add(line);
        }
        double[] karray = new double[tempLines.size()]; // 计算斜率
        int index = 0;
        int[] labels = new int[karray.length];
        for (Line oneLine : tempLines) {
            labels[index] = index;
            karray[index++] = oneLine.getSlope();
        }
        for (int i = 0; i < karray.length - 1; i++) { // 合并
            for (int j = i + 1; j < karray.length; j++) {
                double distance = Math.abs(karray[i] - karray[j]);
                if (distance < 0.1) {
                    labels[i] = i;
                    labels[j] = i;
                }
            }
        }
        lines.addAll(findMaximaChild(labels, tempLines).values());
    }

    /**
     * 找到极大值的孩子
     *
     * @param labels 标签
     * @param tempLines 临时线路
     * @return {@link Map<Integer, Line>}
     */
    private Map<Integer, Line> findMaximaChild(int[] labels, List<Line> tempLines) {
        Map<Integer, Line> lineMap = new HashMap<>();
        for (int i = 0; i < labels.length; i++) {
            lineMap.put(labels[i], tempLines.get(i));
        }
        return lineMap;
    }

    /**
     * 变换极坐标为平面坐标，并绘制
     *
     * @param value 价值
     * @param r1 r1
     * @param theta θ
     * @return {@link Line}
     */
    private Line drawPolarLine(int value, int r1, int theta) {
        int x1 = 100000;
        int y1 = 0;
        int x2 = 0;
        int y2 = 0;
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                int temp = (int) (x * coslut[theta] + y * sinlut[theta]);
                if ((temp - r1) == 0 && x > x2) {   // 变换坐标并绘制
                    x2 = x;
                    y2 = y;
                }
                if ((temp - r1) == 0 && x < x1) {
                    x1 = x;
                    y1 = y;
                }
            }
        }
        return new Line(x1, y1, x2, y2);
    }

    /**
     * 设置因为附近地区
     *
     * @return {@link double[]}
     */
    private double[] setupCosLUT() {
        coslut = new double[180];
        for (int theta = 0; theta < 180; theta++) {
            coslut[theta] = Math.cos((theta * Math.PI) / 180.0);
        }
        return coslut;
    }

    /**
     * 设置罪附近地区
     *
     * @return {@link double[]}
     */
    private double[] setupSinLUT() {
        sinlut = new double[180];
        for (int theta = 0; theta < 180; theta++) {
            sinlut[theta] = Math.sin((theta * Math.PI) / 180.0);
        }
        return sinlut;
    }

}